﻿using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Miao.UI;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Model;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Unity.Entities.SourceGenerators.Test;

namespace Miao.SourceGen.Learn.Test
{
    public static class CSharpIncrementalGeneratorVerifier<TIncrementalGenerator> where TIncrementalGenerator : IIncrementalGenerator, new()
    {


        /// <summary>
        /// if true a throwing verification test will replace the old source of truth with the correct source of truth.
        /// </summary>
        static bool k_OverrideSourceOfTruthOnTestThrow = false;


        public static async Task VerifySourceGeneratorAsync(string source, string generatedFolderName = "Default", params string[] expectedFileNames)
            => await VerifySourceGeneratorAsync(source, DiagnosticResult.EmptyDiagnosticResults, true, generatedFolderName, expectedFileNames);
        public static async Task VerifySourceGeneratorAsync(string source, params DiagnosticResult[] expected)
            => await VerifySourceGeneratorAsync(source, expected, false);
        public static async Task VerifySourceGeneratorAsync(string source, DiagnosticResult expected, IEnumerable<Assembly> additionalAssembliesOverride)
            => await VerifySourceGeneratorAsync(source, new[] { expected }, additionalAssembliesOverride, false);
        public static async Task VerifySourceGeneratorAsync(string source, DiagnosticResult expected, Assembly additionalAssemblyOverride)
            => await VerifySourceGeneratorAsync(source, new[] { expected }, new[] { additionalAssemblyOverride }, false);
        public static async Task VerifySourceGeneratorAsync(string source, DiagnosticResult[] expected, Assembly additionalAssemblyOverride)
            => await VerifySourceGeneratorAsync(source, expected, new[] { additionalAssemblyOverride }, false);
        public static async Task VerifySourceGeneratorAsync(string source, DiagnosticResult[] expected, IEnumerable<Assembly> additionalAssembliesOverride)
            => await VerifySourceGeneratorAsync(source, expected, additionalAssembliesOverride, false);

        static async Task VerifySourceGeneratorAsync(string source, DiagnosticResult[] expected, bool checksGeneratedSource = true, string generatedFolderName = "Default", params string[] expectedFileNames)//预期文件名
            => await VerifySourceGeneratorAsync(source, expected, new[]{//这三项就是找dell咯
            typeof(UIPanel).Assembly,//仿品的意思
        }, checksGeneratedSource, generatedFolderName, expectedFileNames);

        static async Task VerifySourceGeneratorAsync(string source, DiagnosticResult[] expected, IEnumerable<Assembly> additionalAssembliesOverride, bool checksGeneratedSource = true, string generatedFolderName = "Default", params string[] expectedFileNames)
        {
            // Initial Test setup
            var test = new Test { TestCode = source.ReplaceLineEndings() };
            foreach (var additionalReference in additionalAssembliesOverride)
                test.TestState.AdditionalReferences.Add(additionalReference);

            // Create verification-results folder if not present
            var executingAssemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;

            try
            {
                // Include expected files with name from expectedFileNames from verification-results folder as expected source generated files!
                if (checksGeneratedSource)
                {
                    var generatedFolderPath = Path.Join(executingAssemblyPath, "verification-results", generatedFolderName);
                    Directory.CreateDirectory(generatedFolderPath);
                    var foundExpectedFiles = Directory.EnumerateFiles(generatedFolderPath).Select(Path.GetFileName).Where(expectedFileNames.Contains);
                    var existingSources = foundExpectedFiles.Select(file =>
                    {
                        if (file == null)
                            throw new InvalidOperationException();

                        var fileDir = Path.Join(generatedFolderPath, file);
                        var text = File.ReadAllText(fileDir).ReplaceLineEndings();
                        return (Test.GetTestPath(file), SourceText.From(text, Encoding.UTF8));
                    });
                    test.TestState.GeneratedSources.AddRange(existingSources);
                }

                // Run Test
                test.TestBehaviors = checksGeneratedSource ? TestBehaviors.None : TestBehaviors.SkipGeneratedSourcesCheck;
                test.ExpectedDiagnostics.AddRange(expected);
                await test.RunAsync(CancellationToken.None);
            }
            catch (Exception)
            {
                // Rethrow Quickly if we don't check for generated sources
                if (!checksGeneratedSource || !k_OverrideSourceOfTruthOnTestThrow)
                    throw;

                // Generate Correct Sources on throw
                var (correctSources, originalSource) = await test.GenerateCorrectSources();

                // Asserts an error if what it generated is different than the files input by the test
                var builder = new StringBuilder();
                foreach (var actualFileName in correctSources.Select(s => s.fileName).Where(s => !expectedFileNames.Contains(s)))
                    builder.AppendLine($"Generated \"{actualFileName}\" which is not an expected file, please add to `expectedFileNames`.");
                var expectedFileNameDiffErrorMsg = builder.ToString();
                if (expectedFileNameDiffErrorMsg != string.Empty)
                    Assert.Fail(expectedFileNameDiffErrorMsg);

                // Recreate Testing Folder
                var verificationPath = Path.Join(Path.Combine(executingAssemblyPath, "..", "..", ".."), "verification-results", generatedFolderName);
                if (Directory.Exists(verificationPath))
                {
                    var dir = new DirectoryInfo(verificationPath);
                    dir.Attributes &= ~FileAttributes.ReadOnly;
                    dir.Delete(true); // deletes folders including files in it.
                }
                Directory.CreateDirectory(verificationPath);

                // Write new sources
                var writers = new Task[correctSources.Length + 1];
                for (var i = 0; i < correctSources.Length; i++)
                    writers[i] = File.WriteAllTextAsync(Path.Join(verificationPath, correctSources[i].fileName), correctSources[i].content.ToString());
                writers[^1] = File.WriteAllTextAsync(Path.Join(verificationPath, originalSource.fileName), originalSource.content.ToString());
                Task.WaitAll(writers);

                // Make sure to still throw original error.
                throw;
            }


        }

        class Test : SourceGeneratorTest<MSTestVerifier>
        {
            public Test()
            {
                ReferenceAssemblies = new ReferenceAssemblies("net6.0", new PackageIdentity("Microsoft.NETCore.App.Ref", "6.0.0"), Path.Combine("ref", "net6.0"));
                SolutionTransforms.Add((solution, projectId) =>
                {
                    var compilationOptions = solution.GetProject(projectId)?.CompilationOptions;
                    if (compilationOptions == null) throw new ArgumentException("ProjectId does not exist");
                    compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(
                        compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
                    solution = solution.WithProjectCompilationOptions(projectId, compilationOptions);

                    return solution;
                });
            }

            /// <summary>
            /// Folder used by TSourceGenerator to output generated source-files into.
            /// The test expects file-paths to match.
            /// </summary>
            /// <param name="filename">
            /// Filename of file we expect generator to have created.
            /// </param>
            /// <returns>relative filepath of source generated files</returns>
            public static string GetTestPath(string filename)
                => Path.Join(GetFilePathPrefixForGenerator(typeof(TIncrementalGenerator)), filename);

            public async Task<((string fileName, SourceText content)[] generatedSources, (string fileName, SourceText content) source)> GenerateCorrectSources()
            {
                // Initial Setup
                var cancellationToken = CancellationToken.None;
                var fixableDiagnostics = ImmutableArray<string>.Empty;
                var testState = TestState.WithInheritedValuesApplied(null, fixableDiagnostics).WithProcessedMarkup(MarkupOptions, null, ImmutableArray<DiagnosticDescriptor>.Empty, fixableDiagnostics, DefaultFilePath);
                var sourceGenerators = GetSourceGenerators().ToImmutableArray();

                // Create project with applied generators
                var project = await CreateProjectAsync(new EvaluatedProjectState(testState, ReferenceAssemblies), testState.AdditionalProjects.Values.Select(additionalProject => new EvaluatedProjectState(additionalProject, ReferenceAssemblies)).ToImmutableArray(), cancellationToken);
                (project, _) = await ApplySourceGeneratorAsync(sourceGenerators, project, Verify, cancellationToken).ConfigureAwait(false);

                // Splits project.Documents output into 'Generated Files' and Original Source Input
                var generatedSources = project.Documents
                    .Where(d => d.Name.Contains(".g.cs"))
                    .Select(async doc => (Path.GetFileName(doc.Name), await GetSourceTextFromDocumentAsync(doc, cancellationToken).ConfigureAwait(false)));
                return (generatedSources.Select(t => t.Result).ToArray(),
                    (Path.GetFileName(project.Documents.First().Name), await GetSourceTextFromDocumentAsync(project.Documents.First(), cancellationToken).ConfigureAwait(false)));
            }

            #region Copied From Roslyn!

            /// <summary>
            /// Based on <see cref="GeneratorDriver.GetFilePathPrefixForGenerator"/> which is internal.
            /// </summary>
            static string GetFilePathPrefixForGenerator(Type sourceGenType)
                => Path.Combine(sourceGenType.Assembly.GetName().Name ?? string.Empty, sourceGenType.FullName!);

            /// <summary>
            /// <see cref="SourceGeneratorTest{TVerifier}.ApplySourceGeneratorAsync"/> is private so this is a copy of it
            /// </summary>
            async Task<(Project project, ImmutableArray<Diagnostic> diagnostics)> ApplySourceGeneratorAsync(ImmutableArray<ISourceGenerator> sourceGenerators, Project project, IVerifier verifier, CancellationToken cancellationToken)
            {
                var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
                verifier.True(compilation is { });

                var driver = CreateGeneratorDriver(project, sourceGenerators).RunGenerators(compilation, cancellationToken);
                var result = driver.GetRunResult();

                var updatedProject = project;
                foreach (var tree in result.GeneratedTrees)
                {
                    updatedProject = updatedProject.AddDocument(tree.FilePath, await tree.GetTextAsync(cancellationToken).ConfigureAwait(false), filePath: tree.FilePath).Project;
                }

                return (updatedProject, result.Diagnostics);
            }

            /// <summary>
            /// <see cref="SourceGeneratorTest{TVerifier}.GetSourceTextFromDocumentAsync"/> is private so this is a copy of it
            /// </summary>
            static async Task<SourceText> GetSourceTextFromDocumentAsync(Document document, CancellationToken cancellationToken)
            {
                var simplifiedDoc = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
                var formatted = await Formatter.FormatAsync(simplifiedDoc, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
                return await formatted.GetTextAsync(cancellationToken).ConfigureAwait(false);
            }

            #endregion

            #region Create SourceGenerator

            static readonly LanguageVersion DefaultLanguageVersion =
                Enum.TryParse("Default", out LanguageVersion version) ? version : LanguageVersion.CSharp6;
            protected override IEnumerable<ISourceGenerator> GetSourceGenerators()
                => new[] { new TIncrementalGenerator().AsSourceGenerator() };
            protected override string DefaultFileExt => "cs";
            public override string Language => LanguageNames.CSharp;
            protected override GeneratorDriver CreateGeneratorDriver(Project project, ImmutableArray<ISourceGenerator> sourceGenerators)
            {
                return CSharpGeneratorDriver.Create(
                    sourceGenerators,
                    project.AnalyzerOptions.AdditionalFiles,
                    (CSharpParseOptions)project.ParseOptions!,
                    project.AnalyzerOptions.AnalyzerConfigOptionsProvider);
            }
            protected override CompilationOptions CreateCompilationOptions()
                => new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true);
            protected override ParseOptions CreateParseOptions()
                => new CSharpParseOptions(DefaultLanguageVersion, DocumentationMode.Diagnose);

            #endregion
        }
    }
}
